package com.andox.fn.inf.rpc.rest.protocol;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;

import com.alibaba.fastjson.JSON;
import com.andox.fn.inf.rpc.rest.HttpRemotingService;
import com.andox.fn.inf.rpc.rest.RequestContext;
import com.andox.fn.inf.rpc.rest.RequestContextConstants;
import com.andox.fn.inf.rpc.rest.ResponseCodes;
import com.andox.fn.inf.rpc.rest.RpcRequest;
import com.andox.fn.inf.rpc.rest.RpcResponse;
import com.andox.fn.inf.rpc.rest.common.TranslatorWeaver;
import com.andox.fn.inf.rpc.rest.config.InterfaceConfig;
import com.andox.fn.inf.rpc.rest.config.ReferenceConfig;
import com.andox.fn.inf.rpc.rest.extension.Extension;
import com.andox.fn.inf.rpc.rest.extension.ExtensionLoader;
import com.andox.fn.inf.rpc.rest.filter.Invoker;
import com.andox.fn.inf.rpc.rest.proxy.RestServiceWrapper;
import com.andox.fn.inf.rpc.rest.proxy.ServiceBean;
import com.andox.fn.inf.rpc.rest.serializer.Serializer;
import com.andox.fn.inf.rpc.rest.util.BeanNameUtil;
import com.andox.fn.inf.rpc.rest.util.NetUtil;


/**
 * client: rest
 * server: rest controller
 * @author Andox
 *
 */
@Extension("restcontroller")
public class RestControllerProtocol implements Protocol{
	private final static Logger logger = LoggerFactory.getLogger(RestControllerProtocol.class);
	
	private final static String SPRINGBOOT_SERVER_PORT = "server.port";
	private final static String SPRINGBOOT_CONTEXT_PATH = "server.context-path";
	
	private AtomicBoolean exported = new AtomicBoolean(true);
	
	private TranslatorWeaver translatorWeaver = new TranslatorWeaver();
	
	@Override
	public Invoker refer(ReferenceConfig referenceConfig) {
		
		Invoker invoker = new Invoker() {
			Serializer serializer = ExtensionLoader.getExtensionLoader(Serializer.class).getExtension(referenceConfig.getSerialization());

			private HttpRemotingService remotingService = new HttpRemotingService();
			
			@Override
			public RpcResponse invoke(RpcRequest request) {
				Method method = RequestContext.instance().getTProperty(RequestContextConstants.JAVA_METHOD);
				
				String baseUrl = referenceConfig.getBaseUrl();
				if (StringUtils.isNoneBlank(referenceConfig.getRegistry())) {
					StringBuilder sb = new StringBuilder();
					sb.append("http://").append(referenceConfig.getIp()).append(":").append(referenceConfig.getPort());
					if (referenceConfig.getContextpath() != null) {
						sb.append("/").append(referenceConfig.getContextpath());
					}
					baseUrl = sb.toString();
				}
				String url = baseUrl + "/" + this.buildUrl(method);
				
				RequestContext.instance().putProperty(RequestContextConstants.REQUEST_URL, url);
				RequestContext.instance().putProperty(RequestContextConstants.RETURN_TYPE, method.getReturnType());
				RequestContext.instance().putProperty(RequestContextConstants.TIMEOUT_IN_MILLISECONDS, referenceConfig.getTimeout());
				
				Object[] args = RequestContext.instance().getTProperty(RequestContextConstants.REQUEST_PARAM);
				request = this.buildRequest(method, args, url, referenceConfig.getUniqueId());
				RequestContext.instance().putProperty(RequestContextConstants.SERIALIZER, this.serializer);
				
				if (referenceConfig.isAsync()) {
					remotingService.invokeAsync(request, referenceConfig.getTimeout(), null);
				}else if (referenceConfig.isOneway()) {
					remotingService.invokeOneway(request);
				}else {
					RpcResponse biz = remotingService.invokeSync(request, referenceConfig.getTimeout()); //.recreate(serializer);
					return biz;
				}
				
				return RpcResponse.newInstance(request.getOpaque(), ResponseCodes.SUCCESS, "", "");
			}
			
			@Override
			public InterfaceConfig getConfig() {
				return referenceConfig;
			}
			
			private String buildUrl(Method method) {
				StringBuilder sb = new StringBuilder(method.getDeclaringClass().getName());
				sb.append("/").append(method.getName());
				for (Parameter p : method.getParameters()) {
					sb.append("/").append(p.getType().getName());
				}
				
				return sb.toString();
			}
			
			private RpcRequest buildRequest(Method method, Object[] args, String url, String uniqueId) {
				RpcRequest ar = RpcRequest.newInstanceWithId();
				ar.setUniqueId(uniqueId);
				ar.setMethod(method.getName());
				Parameter[] ps = method.getParameters();
				if (ps == null || ps.length == 0) {
					return ar;
				}
				
				Map<String, Object> data = new HashMap<>(); 
				for (int i = 0; i < ps.length; i++) {
					data.put(String.valueOf(i), serializer.serializeAsString(args[i]));
				}
				
				ar.setData(JSON.toJSONString(data));
				
				return ar;
			}
		};
		
		return invoker;
	}

	@Override
	public Invoker export(Invoker invoker) {
		if (!this.exported.compareAndSet(false, true)) {
			logger.error("springboot request mapping has been exported, forbidden export repeat.");
			return invoker;
		}
		
		Class<?> controllerClass = this.translatorWeaver.compileSpringController(invoker.getConfig().getInterfaceClass());
		try {
			BeanDefinitionBuilder bdb = BeanDefinitionBuilder.genericBeanDefinition(RestServiceWrapper.class);
			bdb.addConstructorArgValue(invoker);
			String id = BeanNameUtil.next(RestServiceWrapper.class);
			
			BeanDefinitionBuilder bdb2 = BeanDefinitionBuilder.genericBeanDefinition(controllerClass);
			bdb2.addPropertyReference("serviceWrapper", id);
			
			ServiceBean sb = (ServiceBean)invoker.getConfig();
			DefaultListableBeanFactory bf = (DefaultListableBeanFactory)sb.getApplicationContext().getAutowireCapableBeanFactory();
			bf.registerBeanDefinition(id, bdb.getBeanDefinition());
			bf.registerBeanDefinition(BeanNameUtil.next(controllerClass), bdb2.getBeanDefinition());
			
			
			String p = sb.getApplicationContext().getEnvironment().getProperty(SPRINGBOOT_SERVER_PORT);
			String ip = NetUtil.getIp();
			String cp = sb.getApplicationContext().getEnvironment().getProperty(SPRINGBOOT_CONTEXT_PATH);
			
			sb.setContextpath(cp);
			sb.setIp(ip);
			sb.setPort(p);
			export2Registry(invoker);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		
		return invoker;
	}
	
	private void export2Registry(Invoker invoker){
		String registry = invoker.getConfig().getRegistry();
		if (!StringUtils.isNotBlank(registry))
			return;
		
		ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(ProtocolObjectFactory.REGISTRY).export(invoker);
	}
}
